package hujz.java.file_server.http;

import hujz.java.file_server.Configuration;
import hujz.java.file_server.FileMeta;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedFile;
import io.netty.util.ReferenceCountUtil;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.util.List;
import java.util.Map;

import static hujz.java.file_server.Configuration.INSTANCE;
import static io.netty.handler.codec.http.HttpResponseStatus.*;


// Range: bytes=200-299
// Content-Range: bytes 200-299/11819999846
// Range: bytes=0-50, 100-150 不支持
public class HttpStaticFileServerHandler extends ChannelInboundHandlerAdapter {

    private static Logger LOGGER = LoggerFactory.getLogger(HttpStaticFileServerHandler.class);

    private final String uri;
    private final String contentType;
    private final String fileName;
    private HttpResponse response;
    private long[] range;
    private String rangeHead;
    private File file;

    public HttpStaticFileServerHandler(ChannelHandlerContext ctx, HttpRequest request, HttpResponse response) {
        QueryStringDecoder decoderQuery = new QueryStringDecoder(request.uri());
        Map<String, List<String>> uriAttributes = decoderQuery.parameters();
        String fileId = uriAttributes.get("fileId").get(0);
        FileMeta fileMeta = INSTANCE.getFileMetaSaveImpl().get(fileId);
        this.uri = fileMeta.getUri();
        this.file = Configuration.INSTANCE.getStorageImpl().file(uri);
        this.contentType = fileMeta.getMediaType();
        this.fileName = fileMeta.getFileName();
        this.response = response;
        if (!getRange(ctx, request, uriAttributes, response)) {
            return;
        }
    }

    public boolean getRange(ChannelHandlerContext ctx, HttpRequest request, Map<String, List<String>> uriAttributes, HttpResponse response) {
        String rangeStr = request.headers().get(HttpHeaderNames.RANGE);
        if (rangeStr == null || rangeStr.length() == 0) {
            List<String> rangeHead = uriAttributes.get("range");
            if (rangeHead != null && rangeHead.size() >= 1) {
                rangeStr = uriAttributes.get("range").get(0);
            }
        } else {
            rangeHead = rangeStr;
            String[] items = StringUtils.split(rangeStr, '=');
            if (items.length == 2)
                rangeStr = items[1];
            else if (items.length == 1)
                rangeStr = items[0];
            else {
                sendError(ctx, REQUESTED_RANGE_NOT_SATISFIABLE);
                return false;
            }
        }
        if (rangeStr != null) {
            long fileLength = file.length();
            if (rangeStr.charAt(0) == '-') {
                try {
                    long rangeHead = Long.parseLong(rangeStr);
                    range = new long[]{fileLength + rangeHead, ~rangeHead + 1, fileLength - 1};
                } catch (Exception e) {
                    LOGGER.error("[FileServer] parse(\"" + rangeStr + "\") to long failed!", e);
                    sendError(ctx, REQUESTED_RANGE_NOT_SATISFIABLE);
                    return false;
                }
            } else {
                String[] items = StringUtils.split(rangeStr, '-');
                try {
                    if (items.length == 1) {
                        long start = Long.parseLong(items[0]);
                        range = new long[]{start, fileLength - start, fileLength};
                    } else if (items.length == 2) {
                        long start = Long.parseLong(items[0]), end = Long.parseLong(items[1]);
                        if (start > end) {
                            sendError(ctx, REQUESTED_RANGE_NOT_SATISFIABLE);
                            return false;
                        }
                        range = new long[]{start, end - start + 1, end};
                    } else {
                        sendError(ctx, REQUESTED_RANGE_NOT_SATISFIABLE);
                        return false;
                    }
                } catch (Exception e) {
                    LOGGER.error("[FileServer] parse(\"" + items + "\") to long failed!", e);
                    sendError(ctx, REQUESTED_RANGE_NOT_SATISFIABLE);
                    return false;
                }
            }
        }
        return true;
    }

    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        try {
            if (uri == null) {
                sendError(ctx, FORBIDDEN);
                return;
            }

            File file = Configuration.INSTANCE.getStorageImpl().file(uri);
            if (file.isDirectory() || !file.exists()) {
                sendError(ctx, NOT_FOUND);
                return;
            }
            if (!file.isFile()) {
                sendError(ctx, FORBIDDEN);
                return;
            }


            LOGGER.info("[FileServer] --> Start down file, fileName: \"{}\".", fileName);
            long fileLength = file.length();
            response.headers().set("Content-Type", this.contentType);
            if (rangeHead == null)
                response.headers().set("Accept-Ranges", "bytes");

            try {
                response.headers().add("Content-Disposition", "attachment;filename=\"" + new String(fileName.getBytes("utf-8"), "iso8859-1") + "\"");
            } catch (Exception e) {
                e.printStackTrace();
                // never
            }

            // Write the content.
            ChannelFuture sendFileFuture;
            if (ctx.pipeline().get(SslHandler.class) == null) {
                if (range != null) {
                    response.setStatus(PARTIAL_CONTENT);
                    response.headers().set("Content-Length", range[1]);
                    response.headers().set("Content-Range", "bytes " + range[0] + "-" + range[2] + "/" + fileLength);
                    // Write the initial line and the header.
                    ctx.write(response);
                    sendFileFuture = ctx.write(new DefaultFileRegion(file, range[0], range[1]), ctx.newProgressivePromise());
                } else {
                    // Write the initial line and the header.
                    response.headers().set("Content-Length", fileLength);
                    ctx.write(response);
                    sendFileFuture = ctx.write(new DefaultFileRegion(file, 0, fileLength), ctx.newProgressivePromise());
                }
                ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
            } else {
                // Write the initial line and the header.
                response.headers().set("Content-Length", fileLength);
                ctx.write(response);
                sendFileFuture = ctx.writeAndFlush(new HttpChunkedInput(new ChunkedFile(file, 8192)), ctx.newProgressivePromise());
            }

            sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
                @Override
                public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) {
                    if (total < 0) {
                        LOGGER.trace("[FileServer] --- Transfer progress: " + progress);
                    } else {
                        LOGGER.trace("[FileServer] --- Transfer progress: " + progress + " / " + total);
                    }
                }

                @Override
                public void operationComplete(ChannelProgressiveFuture future) {
                    LOGGER.info("[FileServer] <-- Transfer complete, fileName: \"{}\".", fileName);
                    future.channel().flush();
                }
            }).addListener(ChannelFutureListener.CLOSE);
        } finally {
            ReferenceCountUtil.release(msg);
        }

    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        sendError(ctx, INTERNAL_SERVER_ERROR);
        LOGGER.error(cause.getMessage(), cause);
        LOGGER.info("[FileServer] <-- Download file error, fileName: \"{}\"!", fileName);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        LOGGER.info("[FileServer] <-- Download file compile, fileName: \"{}\"!", fileName);
        super.channelInactive(ctx);
    }

    private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
        response.setStatus(status);
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

}
